# -*- coding: utf-8 -*-
import re
import logging
import os,sys
import simplejson as json
import sqlite3
try:
  import winreg
except:
  import winreg_dummy as winreg
  
env = os.environ.get
TEMP=env('TEMP') or '.'
FAKE_UA="Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)"
DOWNLOAD_LOCATION = env('DOWNLOAD_DIR') or TEMP
DOWNLOAD_CMD='wget -O "%(path)s" -N -c --user-agent="%(ua)s" "%(url)s"'
#DOWNLOAD_CMD='curl -o "%(path)s" -C - "%(url)s"'
APP_DIR=env('APPDATA') or env('USERPROFILE') or '.'
APP_DIR=APP_DIR.decode(sys.getfilesystemencoding())
APP_DIR=os.path.join(APP_DIR,'win-pkg')
DEFAULT_REPOSITORIES = env('REPOSITORIES') or 'main'
STANDALONE_EXE_DIR =  env('STANDALONE_EXE_DIR') or env('SystemRoot')
REPO_URL="http://win-pkg.appspot.com/repository/%s/"
#REPO_URL="http://localhost:8000/repository/main/"
DB_FILE=os.path.join(APP_DIR,'packages.db')
MENU_START=winreg.RegKey(winreg.HKEY_LOCAL_MACHINE,r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")["Common Programs"]

def append_opts(cmd, opts):
  return '"%s" %s'%(cmd, opts) if os.path.exists(cmd) else "%s %s"%(cmd, opts)

var_re=re.compile('\$\(([^\)]+)\)')    
def var_subst(s, d):
  return var_re.sub(r'%(\1)s',s.replace('%','%%'))%d

class PackageMeta(object):
  def __init__(self, meta, repo):
    meta.setdefault('installer_name',meta['name'])
    meta.setdefault('installer_name_regex',None)
    self.download_urls = filter(bool,(meta.get('url%d'%(i+1)) for i in range(3)))
    for k in meta:
      setattr(self, k, meta[k])
    self.repo = repo
      
    if self.installer_name_regex:
      self.installer_name_regex = re.compile(self.installer_name_regex)
    
  def is_installed(self, force = False):
    if force or not hasattr(self,'_installed'):
      self._installed = None
      #TODO - _installed_by* uaktualniane w property repo.installed_cache
      installed_cache = self.repo.installed_cache
      if self.upgrade_guid in self.repo._installed_by_upgradeguid:
        self._installed = self.repo._installed_by_upgradeguid[self.upgrade_guid]
        return True
      if self.installer_name in self.repo._installed_by_name:
        self._installed = self.repo._installed_by_name[self.installer_name]
        return True
        
      def match(i):
        return self.installer_name_regex and self.installer_name_regex.match(i['DisplayName'])
        
      installed = [i for i in installed_cache if match(i)]
      if len(installed)>1:
        """jeżeli więcej niż jeden pasujący, sortujemy malejąco po wersji i bierzemy pierwszy"""
        installed = sorted(installed, cmp=(lambda i1, i2:self.ver_cmp(i1['DisplayVersion'],i2['DisplayVersion'])))
      self._installed = installed and installed[0]
    return bool(self._installed)
  
  @property
  def installed_version(self):
    return self._installed and self._installed['DisplayVersion'] or ''
    
  def __unicode__(self):
    return u"%s:%s-%s"%(self.repository,self.name, self.version)
  
  def __repr__(self):
    return "<%s>"%unicode(self)
  
  @classmethod
  def ver_cmp(cls, v1, v2):
    v1 = v1 and v1.split('.') or ()
    v2 = v2 and v2.split('.') or ()

#what about  _cmp('0.1a','0.10') ??
    def _cmp(a,b):
      try:
        return -cmp(int(a), int(b))
      except:
        return -cmp(a, b)

    for a,b in zip(v1,v2):
      if a!=b:
        return _cmp(a,b)
    return _cmp(len(v1), len(v2))
  
  @classmethod
  def cmp(cls, p1, p2):
    ret = cls.ver_cmp(p1.version, p2.version)
    if ret: return ret
    if p1.repository == p2.repository: return 0
    repos = self.repo.repositories
    for name in repos:
      if p1.repository == name: return -1
      if p2.repository == name: return 1
    return 0
    
  def is_newer(self):
    return self._installed and self.ver_cmp(self.version or '', self.installed_version)<0
  
  def get_ext(self, url):
    if self.download_type:
      return 'msi' if self.download_type=='msi' else 'exe'
    import urlparse
    parsed = urlparse.urlparse(url)
    fn = parsed[2].split('/')[-1].lower()
    return fn.endswith('.msi') and 'msi' or 'exe'
    
  def download(self):
    import urlparse
    try:
      urls = filter(bool,[self.download_url_1, self.download_url_2, self.download_url_3])
      if not urls:
        logging.warning('no download url, skipping')
        return
      import random
      url = random.choice(urls)
      url = var_subst(url, self.__dict__)
      ext = self.get_ext(url)
      path = os.path.join(DOWNLOAD_LOCATION,"%s-%s.%s"%(self.name, self.version, ext))
      args={'path':path,'url':url, 'ua':FAKE_UA}
      cmd = DOWNLOAD_CMD%args
      logging.debug('download using: %s',cmd)
      if os.system('"%s"'%cmd)==0:
        return path
    except Exception,e:
      logging.exception(e)

  def register_standalone_exe(self, dest_path):
    logging.debug('register standalone exe')
    inst = winreg.RegKey(winreg.HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
    inst = inst.create_subkey("%s-standalone"%self.name)
    inst['DisplayVersion'] = self.version
    inst['DisplayName'] = self.name
    inst['DisplayIcon'] = dest_path
    inst['UninstallString'] = 'win-pkg --remove-standalone %s'%self.name
    inst['WinPkgStandaloneExe']=dest_path

    import win32com.client
    shell = win32com.client.Dispatch("WScript.Shell")
    shortcut = shell.CreateShortCut(os.path.join(MENU_START,"%s.lnk"%self.name))
    shortcut.Targetpath = dest_path
    shortcut.save()   
    
  def remove_standalone(self):
    if self._installed['WinPkgStandaloneExe']:
      logging.debug('uninstalling standalone exe')
      os.remove(self._installed['WinPkgStandaloneExe'])
      os.remove(os.path.join(MENU_START,"%s.lnk"%self.name))
      self._installed.remove()
    else:
      logging.warning('%s is not win-pkg standalone executable',self.name)

  def install(self, download_only=False):
    path = self.download()
    if download_only:
      logging.debug('download only, skipping install')
      return True

    if path:
      ext = path[-3:].lower()
      if ext=='msi':
        install_cmd = 'msiexec /qn /norestart /i "%s"'%path
      elif self.download_type=='standalone-exe':
        install_cmd = 'copy /b "%s" "%s"'%(path, STANDALONE_EXE_DIR)
      else:
        install_cmd = '"%s"'%path
        if self.silent_install_options:
          install_cmd = append_opts(install_cmd, self.silent_install_options)
        elif self.autoit_install_script:
          install_cmd = self.build_autoit_cmd(install_cmd) or install_cmd
          
      logging.info('install cmd: %s',install_cmd)
      if os.system('"%s"'%install_cmd)!=0:
        logging.error('install error')
        return
      if self.download_type=='standalone-exe':
        self.register_standalone_exe(os.path.join(STANDALONE_EXE_DIR,os.path.basename(path)))
      self.repo.reload_installed()
      installed = self.is_installed(force=True)
      if installed and self.version and not self.installed_version:
        logging.info("storing version information")
        self._installed.store_version(self.version)
    else:
      logging.debug('download: empty path')

  def build_autoit_cmd(self, cmd, uninstall=False):
    try:
      script = self.autoit_uninstall_script if uninstall else self.autoit_install_script
      if not script: return None
      script = var_subst(script, {'command':"'%s'"%cmd})
      autoit_path = self.repo.autoit_path()
      logging.debug('autoit path:%s',autoit_path)
      if autoit_path:
        sfile = os.path.join(TEMP,'%s-%s%s.au3'%(self.name, self.version,uninstall and '-uninstall' or ''))
        f=open(sfile,'w')
        f.write(script.encode('utf-8'))
        f.close()
        return '"%s" "%s"'%(self.repo.autoit_path(),sfile)
    except Exception, e:
      logging.exception(e)
      return None
      
  def uninstall(self):
    if self.is_installed():
      uninstall_cmd = self._installed['UninstallString']
      if not uninstall_cmd:
        logging.error('no UninstallString registry key, stop')
        return
      if uninstall_cmd.lower().startswith('msiexec'):
        uninstall_cmd='msiexec /uninstall "%s" /qn'%self._installed.name
      else:
        if self.silent_uninstall_options:
          uninstall_cmd = append_opts(uninstall_cmd, self.silent_uninstall_options)
        elif self.autoit_uninstall_script:
          uninstall_cmd = self.build_autoit_cmd(uninstall_cmd, uninstall=True) or uninstall_cmd
      logging.debug("uninstall cmd: %s"%uninstall_cmd)
      ex=os.system('"%s"'%uninstall_cmd)
      if ex!=0:
        logging.error('uninstall error: %d',ex)
      else:
        return True

class Repo(object):
  CREATE_TABLE = """create table if not exists
  packages(id text primary key, 
          modified text,
          repository text,
          name text,
          description text,
          version text,
          download_url_1 text,
          download_url_2 text,
          download_url_3 text,
          download_type text,
          upgrade_guid text,
          installer_name text,
          installer_name_regex text,
          silent_install_options text,
          silent_uninstall_options text,
          autoit_install_script text,
          autoit_uninstall_script text,
          deleted integer default 0
  )"""
  def __init__(self, repositories = None):
    if not os.path.exists(APP_DIR):
      os.makedirs(APP_DIR)
    self.db=sqlite3.connect(DB_FILE.encode('utf-8'))
    self.db.execute(self.CREATE_TABLE)
    self.db.commit()
    self.repositories = repositories or ('main',)

  def empty(self):
#    self.db.execute("delete from packages")
#    self.db.commit()
     self.db.execute('drop table packages')
     self.db.execute(self.CREATE_TABLE)
     self.db.commit()
  
  def last_modified(self, repo):
    result = self.execute_query('select max(modified) from packages WHERE (1=1)',repos = (repo,))
    return result.fetchone()[0]
  
  def update(self, p):
    cols = ('id','modified','repository', 'name','version',
                      'download_url_1','download_url_2','download_url_3', 'download_type',
                      'upgrade_guid','installer_name','installer_name_regex','deleted',
                      'silent_install_options', 'silent_uninstall_options',
                      'autoit_install_script', 'autoit_uninstall_script')
    data = map(p.get,cols)
    logging.info('%(name)s-%(version)s',p)
    logging.debug('%s',repr(p))
    self.db.execute('insert or replace into packages(%s) values(%s)'%(','.join(cols),','.join(('?',)*len(cols))), data)
    
  def commit(self):
    self.db.commit()

  def execute_query(self, q, args = None, repos=None):
    args = args or ()
    if repos:
      q = q.replace('WHERE','WHERE repository in (%s) and'%(','.join('?'*len(repos))))
      args = tuple(repos)+tuple(args)
    return self.db.execute(q,args) if args else self.db.execute(q)
    
  
  def query(self, q, args = None):
    global options
    repos = self.repositories
    result = self.execute_query(q, args, repos = repos)
    header = tuple(d[0] for d in result.description)
    for row in result:
      yield PackageMeta(dict(zip(header,row)), self)
    
  def all(self):
    return self.query("SELECT * from packages WHERE not deleted ORDER BY name,version")
    
  def get(self, name):
    result = list(self.query("SELECT * from packages WHERE not deleted and name=?",(name,)))
    if len(result)>1:
      logging.warning('more than 1 result, returning first')
      result = sorted(result, cmp = PackageMeta.cmp)
      logging.info(result)
    return result and result[0]

  def to_upgrade(self):
    return (i for i in self.installed() if i.is_newer())

  @property
  def installed_cache(self):
    if hasattr(self, '_installed_cache'): return self._installed_cache
    self._installed_cache = []
    self._installed_by_keyname = {}
    self._installed_by_name = {}
    self._installed_by_upgradeguid = {}
    
    for hkey in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
      for prod in winreg.RegKey(hkey, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall"):
        if prod['DisplayName'] and prod['ParentKeyName']!="OperatingSystem":
          self._installed_cache.append(prod)
          self._installed_by_keyname[prod.name] = prod
          self._installed_by_name[prod['DisplayName']] = prod

# {073353ED-8DB6-40E1-BB6D-05007F57A06D}
    def _rev(s):
      while True:
        a,b=s.next(),s.next()
        yield b; yield a
    def rev(s): return ''.join(_rev(iter(s)))
        
    def fmt_guid(s):
      return "{%s-%s-%s-%s-%s}"%(
       s[0:8][::-1],s[8:12][::-1],s[12:16][::-1],rev(s[16:20]),rev(s[20:])
      )

    _guid_by_keyname={}      
    for hkey in (winreg.HKEY_LOCAL_MACHINE,):
      UPGR_CODES="Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\UpgradeCodes"
      for upgr_guid in winreg.RegKey(hkey, UPGR_CODES):
        for value in upgr_guid.values():
          g=fmt_guid(value.name)
          if g in self._installed_by_keyname:
            prod = self._installed_by_keyname[g]
            guid = fmt_guid(upgr_guid.name)
            _guid_by_keyname[g] = guid
            self._installed_by_upgradeguid[guid] = prod

    if logging.root.level<=logging.DEBUG:
      for p in sorted(self._installed_cache, cmp=(lambda a,b:cmp(a['DisplayName'],b['DisplayName']))):
        logging.debug('%s (%s)',p['DisplayName'], p['DisplayVersion'])
        guid = _guid_by_keyname.get(p.name)
        if guid: logging.debug('guid: %s',guid)
    return self._installed_cache

  def reload_installed(self):
    if hasattr(self, '_installed_cache'):
      delattr(self, '_installed_cache')
  
  def installed(self):
    return (i for i in self.all() if i.is_installed())    

  def autoit_path(self):
    if hasattr(self,'_autoit_path'): return self._autoit_path
    meta = self.get('autoit3')
    if meta and meta.is_installed():
      self._autoit_path=os.path.join(os.path.dirname(meta._installed['UninstallString']),"AutoIt3.exe")
    else:
      self._autoit_path=None
    return self._autoit_path
